#!/usr/bin/perl -w
# $Id$
# vim:sw=2
# vim600:fdm=marker
# This script is in charge of copying files from the local stage to the root
# of the local filesystem

require 5.006;
use warnings FATAL => qw(all);
use strict;
use sigtrap qw(die untrapped normal-signals
               stack-trace any error-signals);

use File::Path;
use File::Find;

use constant LIB_DIR => '/usr/lib/slack';
use lib LIB_DIR;
use Slack;

my @rsync = ('rsync',
             '--relative',
             '--times',
             '--perms',
             '--group',
             '--owner',
             '--links',
             '--devices',
             '--sparse',
             '--no-implied-dirs', # SO GOOD!
             '--files-from=-',
             '--from0',
             );

(my $PROG = $0) =~ s#.*/##;

sub install_files ($);

########################################
# Environment
# Helpful prefix to die messages
$SIG{__DIE__} = sub { die "FATAL[$PROG]: @_"; };
# Set a reasonable umask
umask 077;
# Get out of wherever (possibly NFS-mounted) we were
chdir("/")
  or die "Could not chdir /: $!";
# Autoflush on STDERR
select((select(STDERR), $|=1)[0]);

########################################
# Config and option parsing {{{
my $usage = Slack::default_usage("$PROG [options] <role> [<role>...]");
# Option defaults
my %opt = ();
Slack::get_options(
  opthash => \%opt,
  usage => $usage,
  required_options => [ qw(root stage) ],
);
# }}}

# Arguments are required
die "No roles given!\n\n$usage" unless @ARGV;

unless (-d $opt{root}) {
    if (not $opt{'dry-run'}) {
      eval { mkpath($opt{root}); };
      die "Could not mkpath destination directory '$opt{root}': $@\n" if $@;
    }
    warn "WARNING[$PROG]: Created destination directory '".$opt{root}."'\n";
}

# Prepare for backups
if ($opt{backup} and $opt{'backup-dir'}) {
  # Make sure backup directory exists
  unless (-d $opt{'backup-dir'}) {
    ($opt{verbose} > 0) and print STDERR "Creating backup directory '$opt{'backup-dir'}'\n";
    if (not $opt{'dry-run'}) {
      eval { mkpath($opt{'backup-dir'}); };
      die "Could not mkpath backup dir '$opt{'backup-dir'}': $@\n" if $@;
    }
  }
  push(@rsync, "--backup", "--backup-dir=$opt{'backup-dir'}");
}
# Pass options along to rsync
if ($opt{'dry-run'}) {
  push @rsync, '--dry-run';
}
if ($opt{'verbose'} > 1) {
  push @rsync, '--verbose';
}

# copy over the new files
for my $role (@ARGV) {
  install_files($role);
}
exit 0;

# This subroutine takes care of actually installing the files for a role
sub install_files ($) {
  my ($role) = @_;
  # final / is important for rsync
  my $source = $opt{stage} . "/roles/" . $role . "/files/";
  my $destination = $opt{root} . "/";
  my @command = (@rsync, $source, $destination);
  my ($rsync_pid, $rsync_fh);

  # Try to give some sensible message here
  if ($opt{verbose} > 0) {
    if ($opt{'dry-run'}) {
      print STDERR "$PROG: Dry-run syncing '$source' to '$destination'\n";
    } else {
      print STDERR "$PROG: Syncing '$source' to '$destination'\n";
    }
  }

  # Divide into parent (which will make a list of files to install)
  # and child (which will exec rsync)
  if ($rsync_pid = open($rsync_fh, "|-")) {
    # Parent
  } elsif (defined $rsync_pid) {
    # Child
    # This redirection is necessary because rsync sends
    #   verbose output to STDOUT
    open(STDOUT, ">&STDERR")
      or die "Could not redirect STDOUT to STDERR\n";
    exec(@command);
    die "Could not exec '@command': $!\n";
  } else {
    die "Could not fork: $!\n";
  }

  select((select($rsync_fh), $|=1)[0]);  # Turn on autoflush

  # Find files to rsync
  # Basically, we try to exclude directories, if at all possible,
  # so we're not constantly resetting permissions on /, /etc, and so on.
  find ({
      wanted => sub {
        if (-l or not -d _) {
          # Copy all files, links, etc
          my $file = $File::Find::name;
          # Strip the $source from the path,
          # since rsync expects relative paths
          ($file =~ s#^$source##)
            or die "sub failed: $source|$file";

          #print STDERR ":$file\n";
          print $rsync_fh "$file\0";
        } elsif (-d _) {
          # For directories, we only want to copy it if it doesn't
          # exist in the destination yet.
          my $dir = $File::Find::name;
          # We know the root directory will exist (we make it above),
          # so skip the base of the source
          (my $short_source = $source) =~ s#/$##;
          return if $dir eq $short_source;

          # Strip the $source from the path,
          # since rsync expects relative paths
          ($dir =~ s#^$source##)
            or die "sub failed: $source|$dir";

          if (not -d "$destination/$dir") {
            #print STDERR ":$dir\n";
            print $rsync_fh "$dir\0";
          }
        }
      }
    },
    $source,
  );

  # Check return value
  unless (close($rsync_fh)) {
    Slack::check_system_exit(@command);
  }
}

